package com.hmdp.service.impl;

import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.lock.impl.SimpleRedisLock;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

/**
 * 优惠券秒杀：
 *      -- 秒杀优惠券
 *          -- 正常的流程是判断优惠券在不在有效时间内 接着判断库存够不够 够的话就去做扣减 并生成一个订单
 *      -- 库存超卖问题：
 *          -- 根本原因是判断库存够不够 和 扣减库存没有一起执行
 *          -- 解决方法：加锁
 *              -- 悲观锁：认为一定会出现并发安全问题 会充分的添加好锁 保证线程没有问题 但是影响了性能
 *              -- 乐观锁：认为不会出现并发安全问题 在线程更新前判断下有没有别的线程来修改 比如看看修改的时候的库存是不是等于刚开始查到的
 *                  -- 版本号法：添加一个版本号
 *                  -- CAS 法：看库存有没有变化
 *      -- 一人一单问题：
 *          -- 就是用户发请求过来买东西的时候 你先根据用户 id 和 优惠券 id 从订单表中查询 查到了就说明买过了
 *          -- 注意这里的查询买过没有和下单这个也不是同步的 即会有线程安全问题
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    private final StringRedisTemplate stringRedisTemplate ;
    private final ISeckillVoucherService seckillVoucherService ;    // 操作秒杀优惠券的业务层
    private final RedisIdWorker redisIdWorker ;

    /**
     * 秒杀优惠券    加了乐观锁 一人一单 基于分布式锁
     *
     * @param voucherId
     * @return
     */
    public long seckillVoucher(Long voucherId) {
        // 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 判断秒杀是否已经开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            throw new RuntimeException("秒杀尚未开始！");
        }
        // 判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            throw new RuntimeException("秒杀已经结束！");
        }
        // 判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            throw new RuntimeException("库存不足！");
        }
        Long userId = UserHolder.getUser().getId() ;
        // 创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);// 业务名称前加上 userId 保证每个用户自己锁自己的
        // 获取锁
        boolean isLock = lock.tryLock(RedisConstants.LOCK_SHOP_TTL);
        // 判断是否获取成功
        if (!isLock) {
            // 有锁获取失败 说明你在重复下单
            throw new RuntimeException("不允许重复下单！");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 将判断能不能创建订单 和创建订单的代码放一起加锁
     * @param voucherId
     * @return
     */
    @Transactional
    public Long createVoucherOrder(Long voucherId) {
        // 一人一单     这个逻辑应该写在扣减库存前面 但是因为你加了事务注解 如果这里失败 则会回滚
        // 查询订单
        Long userId = UserHolder.getUser().getId() ;
        long count = this.query().eq("user_id", userId).eq("voucher_id", voucherId).count() ;
        // 判断是否存在
        if (count > 0) {
            // 存在了
            throw new RuntimeException("您已经购买过一次了！");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                // 更新时候的库存值等于我查到的 说明之间没有人修改过
                // .eq("stock", voucher.getStock())
                // 但是常常有线程插入进来的 你这样等于把被插入的线程都干死了 其实只要库存大于 0 就行了 没必要不让他们插进来
                .gt("stock", 0)
                .update();
        if (!success) {
            // 扣减库存失败
            throw new RuntimeException("扣减库存失败！");
        }
        // 创建订单
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .voucherId(voucherId)
                .id(redisIdWorker.nextId("order"))  // 业务前缀
                .userId(UserHolder.getUser().getId())
                .build();
        // 保存订单
        this.save(voucherOrder) ;
        // 返回订单 id
        return voucherOrder.getId();
    }

    /**
     * 秒杀优惠券 最原始的代码
     *
     * @param voucherId
     * @return
     */
    @Transactional
    public long seckillVoucher_01(Long voucherId) {
        // 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 判断秒杀是否已经开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            throw new RuntimeException("秒杀尚未开始！");
        }
        // 判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            throw new RuntimeException("秒杀已经结束！");
        }
        // 判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            throw new RuntimeException("库存不足！");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .update();
        if (!success) {
            // 扣减库存失败
            throw new RuntimeException("扣减库存失败！");
        }
        // 创建订单
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .voucherId(voucherId)
                .id(redisIdWorker.nextId("order"))  // 业务前缀
                .userId(UserHolder.getUser().getId())
                .build();
        // 保存订单
        this.save(voucherOrder) ;
        // 返回订单 id
        return voucherOrder.getId() ;
    }

    /**
     * 秒杀优惠券    加了乐观锁
     *
     * @param voucherId
     * @return
     */
    @Transactional
    public long seckillVoucher_02(Long voucherId) {
        // 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 判断秒杀是否已经开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            throw new RuntimeException("秒杀尚未开始！");
        }
        // 判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            throw new RuntimeException("秒杀已经结束！");
        }
        // 判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            throw new RuntimeException("库存不足！");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                // 更新时候的库存值等于我查到的 说明之间没有人修改过
                // .eq("stock", voucher.getStock())
                // 但是常常有线程插入进来的 你这样等于把被插入的线程都干死了 其实只要库存大于 0 就行了 没必要不让他们插进来
                .gt("stock", 0)
                .update();
        if (!success) {
            // 扣减库存失败
            throw new RuntimeException("扣减库存失败！");
        }
        // 创建订单
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .voucherId(voucherId)
                .id(redisIdWorker.nextId("order"))  // 业务前缀
                .userId(UserHolder.getUser().getId())
                .build();
        // 保存订单
        this.save(voucherOrder) ;
        // 返回订单 id
        return voucherOrder.getId() ;
    }

    /**
     * 秒杀优惠券    加了乐观锁 一人一单
     *
     * @param voucherId
     * @return
     */
    public long seckillVoucher_03(Long voucherId) {
        // 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 判断秒杀是否已经开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            throw new RuntimeException("秒杀尚未开始！");
        }
        // 判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            throw new RuntimeException("秒杀已经结束！");
        }
        // 判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            throw new RuntimeException("库存不足！");
        }
        Long userId = UserHolder.getUser().getId() ;
        synchronized (userId.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }
}
